package be.kdg.bombermanunlimited.server.audiomanager;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.LinkedList;

import be.kdg.bombermanunlimited.audio.rmi.IAudioListener;
import be.kdg.bombermanunlimited.server.network.Node;
import be.kdg.bombermanunlimited.server.network.interfaces.INetworkManagerListener;

public class AudioManager extends UnicastRemoteObject implements IAudioManager,
		INetworkManagerListener {
	/**
	 * Deze klasse kiest welke client audioserver mag zijn en geeft aan wanneer
	 * een andere client de rol moet overnemen. Meestal zal dit zijn wanneer de
	 * huidige audioserver een End of File melding geeft, maar in geval van
	 * wegvallen van de audioserver kan de centrale server zelf ook ingrijpen.
	 * 
	 * @author Siegfried
	 */
	LinkedList<AudioNode> listeners = new LinkedList<AudioNode>();
	private AudioNode currentAudioServer;
	private LinkedList<AudioNode> newlisteners = new LinkedList<AudioNode>();
	private byte id = 0;

	private static final long serialVersionUID = 1067217589997501718L;

	public AudioManager() throws RemoteException {
		super();
	}

	/**
	 * Hiermee kan een client aangeven dat het laatste pakket van zijn
	 * audiostream verzonden is. Om te voorkomen dat een verkeerde server dit
	 * bericht stuurt, wordt ook het ip en de poort van de client meegegeven en
	 * gecontroleerd of dit het socket is van wie volgens de centrale server de
	 * huidige audioserver zou moeten zijn. Deze functie kan rechtstreeks worden
	 * opgeroepen door de audioserver dankzij de interface IAudioManager.
	 */
	@Override
	public synchronized void endOfFile(String ip, int port) {
		if (currentAudioServer == null
				|| currentAudioServer.compareTo(ip, port)) {
			endOfFile();
		}
	}

	/**
	 * Deze functie geeft wordt opgeroepen wanneer er een nieuwe server moet
	 * worden geselecteerd. Deze functie is enkel lokaal beschikbaar en wordt
	 * zowel gebruikt na aanvragen van de client als lokaal gedetecteerde
	 * problemen.
	 */
	private synchronized void endOfFile() {
		System.out.println("[AudioManager] EOF. Looking for new server");
		int index;
		int loopCount = 0;
		while (true) {
			int available = 0;
			// tel hoeveel clients in staat zijn tot streamen.
			for (AudioNode node : listeners) {
				if (node.isServerEnabled()) {
					available++;
				}
			}
			if (available == 0) {
				System.out.println("No audio servers available.");
				return;
			}
			try {
				// selecteer de volgende beschikbare client
				index = nextIndex();
				System.out.println(newlisteners.size());
				currentAudioServer = listeners.get(index);
				currentAudioServer.getListener().startPlaying(++id);
				System.out.println("[AudioManager] index:" + index + "/"
						+ (listeners.size() - 1));
				newlisteners.clear();
				return; // found one. Return.
			} catch (Exception e) {
				// e.printStackTrace();
			}

			// Er ging iets mis. probeer nog een keer na een kleine delay.
			System.out.println("Server selection failed. Delaying.");
			try {
				// short wait time
				Thread.sleep(500);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}
			// Om eindeloze lussen te vermijden wordt er slechts 2x doorheen
			// heel de lijst van clients gelopen. Als er dan nog steeds geen
			// audioserver is wordt geen nieuwe server gezocht tot er een nieuwe
			// node wordt toegevoegd aan het ringnetwerk.
			loopCount++;
			if (loopCount >= 2 * listeners.size()) {
				return;
			}
		}

	}

	/**
	 * Gaat op zoek naar de volgende beschikbare server.
	 * 
	 * @return de index in de lijst van clients.
	 * @throws Exception
	 *             Wanneer de lijst 5x werd doorlopen zonder een kandidaat
	 *             audioserver te vinden, wordt de zoektocht gestopt. Of wanneer
	 *             de lijst met kandidaten leeg is.
	 */
	private synchronized int nextIndex() throws Exception {
		int index = 0;
		// Selecteer de volgende audioserver als er reeds een vorige was
		// ingesteld.
		if (currentAudioServer != null) {
			index = listeners.indexOf(currentAudioServer) + 1;
			System.out.println("[AudioManager] index incremented:" + index);
		}
		boolean searching = true;
		int loopCount = 0;
		while (searching) {
			// controleer of er wel clients in de lijst steken.
			if (listeners.size() <= 0) {
				throw new Exception("Empty list.");
			} else if (index >= listeners.size()) {
				index = 0;
			}
			try {
				// nodes die pas verbonden zijn, worden in een tijdelijke lijst
				// geplaatst en kunnen niet de eerstvolgende audioserver zijn.
				// Dit gaf immers problemen wanneer de aanvraag binnenkwam
				// terwijl de node nog niet volledig geinitialiseerd was. Als de
				// node niet in de lijst met nieuwe nodes staat is hij kandidaat
				// onder voorwaarde dat hij in staat is tot streamen.
				if (!newlisteners.contains(listeners.get(index))) {
					if (listeners.get(index).isServerEnabled()) {
						listeners.get(index).getListener().IsUp();
						return index;
					}
				} else {
					// Verwijder de node van de lijst met nieuwe nodes, vanaf de
					// volgende ronde mag hij audioserver zijn.
					newlisteners.remove(listeners.get(index));
					System.out.println("Rejected new node as audio server.");
					try {
						// short wait time
						Thread.sleep(200);
					} catch (InterruptedException e1) {
						e1.printStackTrace();
					}
				}

			} catch (Exception e) {

			}
			index = index + 1;
			loopCount++;
			if (loopCount >= 5 * listeners.size()) {
				throw new Exception(
						"Infinite loop. Cycled through listeners 5 times");
			}
		}
		return 0;
	}

	/**
	 * Opgeroepen wanneer een nieuwe node wordt toegevoegd aan het ringnetwerk.
	 * De node wordt eveneens op een lijst met nieuwe nodes geplaatst. Nieuwe
	 * nodes komen niet onmiddelijk aan bod voor de rol van audioserver.
	 */
	@Override
	public synchronized void addAudioListener(String ip, int port,
			boolean serverEnabled, IAudioListener listener) {
		AudioNode node = new AudioNode(ip, port, serverEnabled, listener);
		newlisteners.add(node);
		listeners.add(node);
		if (currentAudioServer == null) {
			try {
				Thread.sleep(3000); // give the client some time to start
				endOfFile();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Opgeroepen als er een node wegvalt. De node wordt verwijderd uit de lijst
	 * en indien de node audioserver was, krijgen alle nodes het bevel hun
	 * audiobuffers leeg te maken, alle pakketten van de node in kwestie te
	 * weigeren en wordt er een nieuwe server aangeduid.
	 * 
	 * @param node
	 *            De node die moet verwijderd worden. Vergelijken met de
	 *            audionode gebeurt op basis van de socket.
	 */
	@Override
	public synchronized void removeAudioListener(Node node)
			throws RemoteException {

		AudioNode an = new AudioNode(node.getIp(), node.getPort(), false, null);

		newlisteners.remove(an);

		int index = 0;
		while (index < listeners.size() && !listeners.get(index).equals(an)) {
			index++;
		}

		if (index < listeners.size()) {
			System.out.println("[AudioManager]Removed audio node.");
			listeners.remove(index);

			if (currentAudioServer != null && currentAudioServer.equals(an)) {
				currentAudioServer = null;
				System.out
						.println("[AudioManager]Audio node was server, selecting new server.");
				for (AudioNode n : listeners) {
					n.getListener().flushBuffer();
				}
			}
		}

		if (currentAudioServer == null) {
			endOfFile();
		}
	}

	/**
	 * Met deze functie kunnen nodes aangeven dat ze zich al dan niet
	 * beschikbaar stellen als server. Deze functie wordt niet gebruikt, maar
	 * zou eventueel gebruikt kunnen worden de spelers at runtime alsnog te
	 * laten beslissen om zich kandidaat te stellen voor audiostreams.
	 * 
	 * @param b
	 *            true = ik ben beschikbaar als audioserver
	 * @param ip
	 * @param port
	 *            vormen samen de socket van de audionode in kwestie.
	 */
	@Override
	public synchronized void enableServer(boolean b, String ip, int port)
			throws RemoteException {
		for (AudioNode it : listeners) {
			if (it.compareTo(ip, port)) {
				it.setServerEnabled(b);
			}
			if (currentAudioServer == null) {
				endOfFile();
			}
		}
	}

	/**
	 * Opgeroepen wanneer een node van het ringnetwerk wordt verwijderd.
	 * Verwijdert meteen ook de audiolistener.
	 */
	@Override
	public synchronized void onNodeRemoved(Node node) {
		try {
			removeAudioListener(node);
		} catch (RemoteException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Met deze functie kan een client controleren of hij wel degelijk de
	 * toelating heeft om te zenden. Dit is een extra beveiliging om te
	 * voorkomen dat meerdere of foute clients tegelijk data over het
	 * ringnetwerk zouden sturen.
	 */
	@Override
	public boolean requestToTransmit(String ip, int port, String username)
			throws RemoteException {
		if (currentAudioServer.compareTo(ip, port)) {
			System.out.println("[AudioManager]Request to transmit granted to "
					+ username);
			return true;
		}
		System.out.println("[AudioManager]Request to transmit DENIED to"
				+ username);
		return false;
	}

	/**
	 * Wanneer een client problemen ondervindt bij het starten van de stream kan
	 * hij de rol als server weigeren, dit gebeurt door het oproepenvan
	 * onderstaande functie.
	 * 
	 */
	@Override
	public void declineServerRole() throws RemoteException {
		endOfFile();
	}
}
